﻿using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Threading.Tasks;

using PickGold.Data.Hsts.General.Collections;
using PickGold.Data.Hsts.General.Compression;
using PickGold.Data.Hsts.General.Persist;
using PickGold.Data.Hsts.Records;
using PickGold.Data.Hsts.Database.Operations;
using PickGold.Data.Hsts.WaterfallTree;
using PickGold.Data.Hsts.General.Extensions;

namespace PickGold.Data.Hsts.Database
{
	public class DataContainerPersist : IPersist<IDataContainer>
	{
		public const byte VERSION = 40;

		private IIndexerPersist<IData> keyIndexerPersist;
		private IIndexerPersist<IData> recordIndexerPersist;

		private IPersist<IData> keyPersist;
		private IPersist<IData> recordPersist;

		private IDataContainerFactory containerFactory;

		private bool verticalCompression;

		public DataContainerPersist(IIndexerPersist<IData> keyIndexerPersist, IIndexerPersist<IData> recordIndexerPersist, IDataContainerFactory containerFactory)
		{
			this.keyIndexerPersist = keyIndexerPersist;
			this.recordIndexerPersist = recordIndexerPersist;
			this.containerFactory = containerFactory;
			verticalCompression = true;
		}

		public DataContainerPersist(IPersist<IData> keyPersist, IPersist<IData> recordPersist, IDataContainerFactory containerFactory)
		{
			this.keyPersist = keyPersist;
			this.recordPersist = recordPersist;
			this.containerFactory = containerFactory;
			verticalCompression = false;
		}

		private void WriteRaw(BinaryWriter writer, IDataContainer data)
		{
			lock (data)
			{
				writer.Write(data.Count);
				writer.Write(data.IsInternallyOrdered);

				foreach (var kv in data.InternalEnumerate())
				{
					keyPersist.Write(writer, kv.Key);
					recordPersist.Write(writer, kv.Value);
				}
			}
		}

		private IDataContainer ReadRaw(BinaryReader reader)
		{
			int count = reader.ReadInt32();
			bool isOrdered = reader.ReadBoolean();

			var data = containerFactory.Create();

			KeyValuePair<IData, IData>[] array = new KeyValuePair<IData, IData>[count];

			for (int i = 0; i < count; i++)
			{
				IData key = keyPersist.Read(reader);
				IData record = recordPersist.Read(reader);
				array[i] = new KeyValuePair<IData, IData>(key, record);
			}

			data.LoadFrom(array, count, isOrdered);

			return data;
		}

		private void WriteVertical(BinaryWriter writer, IDataContainer data)
		{
			KeyValuePair<IData, IData>[] rows;

			bool isInternallyOrdered;

			lock (data)
			{
				isInternallyOrdered = data.IsInternallyOrdered;

				rows = new KeyValuePair<IData, IData>[data.Count];
				int index = 0;
				foreach (var kv in data.InternalEnumerate())
					rows[index++] = kv;

				CountCompression.Serialize(writer, checked((ulong)rows.Length));
				writer.Write(data.IsInternallyOrdered);
			}

#if DEBUG
            if (isInternallyOrdered)
            {
                for (int i = 1; i < rows.Length; i++)
                    Debug.Assert(data.Comparer.Compare(rows[i - 1].Key, rows[i].Key) < 0);
            }
#endif

			var actions = new Action[2];
			var streams = new MemoryStream[2];

			actions[0] = () =>
			{
				streams[0] = new MemoryStream();
				keyIndexerPersist.Store(new BinaryWriter(streams[0]), (idx, o) => ((KeyValuePair<IData, IData>[])o)[idx].Key, rows.Length, rows);
			};

			actions[1] = () =>
			{
				streams[1] = new MemoryStream();
				recordIndexerPersist.Store(new BinaryWriter(streams[1]), (idx, o) => ((KeyValuePair<IData, IData>[])o)[idx].Value, rows.Length, rows);
			};

			Parallel.Invoke(actions);

			foreach (var stream in streams)
			{
				using (stream)
				{
					CountCompression.Serialize(writer, checked((ulong)stream.Length));
					writer.Write(stream.GetBuffer(), 0, (int)stream.Length);
				}
			}
		}

		private static readonly KeyValuePairHelper<IData, IData> helper = new KeyValuePairHelper<IData, IData>();

		private IDataContainer ReadVertical(BinaryReader reader)
		{
			var count = (int)CountCompression.Deserialize(reader);
			var isOrdered = reader.ReadBoolean();

			var array = new KeyValuePair<IData, IData>[count];

			var actions = new Action[2];
			var buffers = new byte[2][];

			for (var i = 0; i < buffers.Length; i++)
				buffers[i] = reader.ReadBytes((int)CountCompression.Deserialize(reader));

			actions[0] = () =>
			{
				using (var ms = new MemoryStream(buffers[0]))
					keyIndexerPersist.Load(new BinaryReader(ms), (idx, o, value) => helper.SetKey(ref array[idx], value), count, this);
			};

			actions[1] = () =>
			{
				using (var ms = new MemoryStream(buffers[1]))
					recordIndexerPersist.Load(new BinaryReader(ms), (idx, o, value) => helper.SetValue(ref array[idx], value), count, this);
			};

			var task = Task.Factory.StartNew(actions[1]);
			actions[0]();
			task.Wait();

			var data = containerFactory.Create();
			data.LoadFrom(array, count, isOrdered);

			return data;
		}

		public void Write(BinaryWriter writer, IDataContainer item)
		{
			writer.Write(VERSION);

			if (verticalCompression)
				WriteVertical(writer, item);
			else
				WriteRaw(writer, item);
		}

		public IDataContainer Read(BinaryReader reader)
		{
			if (reader.ReadByte() != VERSION)
				throw new Exception("Invalid DataContainerPersist version.");

			if (verticalCompression)
				return ReadVertical(reader);
			else
				return ReadRaw(reader);
		}
	}
}
